home *** CD-ROM | disk | FTP | other *** search
Text File | 1992-03-11 | 22.5 KB | 442 lines | [TEXT/KAHL] |
- Luminance.doc
-
- HISTORY:
- 3/11/92 dgp added prototypes to supplement the explanations of the routines.
-
- INTRODUCTION:
-
- The purpose of these subroutines is accurate control of contrast of visual
- stimuli for vision experiments. These subroutines set up the Color Lookup Table
- (CLUT) of a video framestore on the Mac II (e.g. the Apple Mac II Color Video
- Card) to properly drive a monochrome monitor (e.g. the Apple High Resolution
- Monochrome Monitor) via an Institute for Sensory Research (ISR) Video
- Attenuator. The ISR Video Attenuator consists of resistive dividers which
- attenuate the three color signal from the video framestore by different
- amounts and combine them to produce one monochrome signal.
-
- The video attenuator and the theory behind the algorithms described here are
- the topic of a paper:
- D.G. Pelli and L. Zhang (1991) Accurate control of contrast on microcomputer displays.
- Vision Research, 31:1337-1360.
-
- Achieving the goal of accurate control of contrast required solving five
- problems:
- 1. The luminance of the monitor is nonlinearly related to the input voltage which
- depends linearly on the triplet
- loaded into the CLUT. This is solved by allowing the user to provide a
- polynomial (I recommend 4th order) describing the nonlinear relationship
- between luminance and nominal voltage. (For computational reasons, you must also supply
- a quadratic fit.) The subroutines LToV and VToL use this polynomial
- to convert back and forth.
- 2. Affordable framestores for microcomputers have 8 bit Digital to Analog
- Converters (DACs), which can only represent 256 different luminances. This is
- not enough precision to produce threshold-contrast patterns, which are
- important for vision experiments. This is solved by the ISR Video Attenuator
- which combines the outputs of the three DACs ("red", "green", and "blue") with
- various attenuations to yield one monochrome signal which can represent low
- contrasts (by varying an attenuated DAC) and high contrasts (by varying an
- unattenuated DAC).
- 3. DACs are only accurate to ± a least significant bit (LSB). The Brooktree DACs
- used by Apple and RasterOps are specified to have a ±1 LSB "integrated linearity"
- error, which means that the voltage produced by any number will deviate from a
- line connecting the 0 and 255 points by at most 1 LSB, which is defined as 1/255
- times the voltage difference between 0 and 255. I assume a slightly stronger
- restriction will hold, that the difference between any two settings will be in
- error by at most one LSB. So it is important to
- allow the user to specify what range of luminances will be used to present a
- particular stimulus and to fix the coarsest DACs (i.e. those with the least
- attenuation in the ISR Video Attenuator) and only vary the fine DACs to produce
- the specified luminances. The range setting is done by SetLuminanceRange()
- which is called automatically by SetLuminance() and SetLuminances().
- 4. Apple's software environment for graphics, QuickDraw, isn't designed for
- vision experiments. The facilities for creating images are fine, but the control
- of the CLUTs provided by the Palette and Color Managers is unsatisfactory for
- vision research because a lot happens behind your back. In particular the
- Palette Manager likes to maintain a consistent color environment across the
- entire desktop, which includes all your screens. Well, in vision experiments we
- generally want to treat each screen as a totally separate device, unaffected by
- what we do to the other screens. This is solved by the routine GDSetEntries()
- which makes a low-level call to the video driver to change the CLUT directly
- without QuickDraw's knowledge. (This will work with any video card
- that works with the Mac II.) The routine LoadLuminances(), which is used below,
- is just a convenient glue routine for calling GDSetEntries().
- 5. Apple specifies that the video driver should silently implement gamma
- correction to attempt to correct for the display's nonlinear relation between
- luminance and input voltage. We don't want this because this hidden gamma
- correction loses precision and interferes with the operation of the ISR Video
- Attenuator, which will seem to operate NONlinearly if this gamma correction
- takes place unbeknownst to us. This is solved by the routine GDLinearGamma()
- which makes a low-level call to the video driver and loads a linear gamma
- table, i.e. no correction.
-
- SUMMARY:
-
- double SetLuminance(GDHandle myGDHandle,luminanceRecord *LP
- ,int theEntry,double luminance
- ,double lowLuminance,double highLuminance)
- Set one entry in the ColorSpec table (and the CLUT if myGDHandle is not NULL) to
- a specified luminance. It's ok for lowLuminance to be greater than highLuminance.
- SetLuminance() sets a single entry to the specified luminance. You must also
- indicate the luminance range that you are working over, to allow optimal choice
- of which dacs to fix, etc., to yield minimum error in relative luminance.
- SetLuminances() does its work by making a call to SetLuminance() for each entry.
- SetLuminance takes 1 ms to set up the range (by calling SetLuminanceRange), which
- it only has to do once. Repeated calls to SetLuminance with the same range will
- not cause re-computation of the range. Once the range is set, SetLuminance takes
- about 0.1 ms. If SetLuminance is too slow for a real-time application you can tell
- it to just compute, but not load the new ColorSpec table (just supply NULL in place
- of myGDHandle), and you can then quickly load your ColorSpec table into the CLUT
- later by calling LoadLuminances().
-
- double SetLuminancesAndRange(GDHandle myGDHandle,luminanceRecord *LP
- ,int firstEntry,int lastEntry
- ,double firstLuminance,double lastLuminance
- ,double lowLuminance,double highLuminance)
- /*
- Set a series of entries in the ColorSpec table (and the CLUT if myGDHandle is not NULL)
- to a linear sequence of luminances.
- Uses last two arguments to set the luminance range of interest.
- */
- double SetLuminances(GDHandle myGDHandle,luminanceRecord *LP
- ,int firstEntry,int lastEntry
- ,double firstLuminance,double lastLuminance)
- /*
- Set a series of entries in the ColorSpec table (and the CLUT if myGDHandle is not NULL)
- to a linear sequence of luminances. Assume this is the entire luminance range of
- interest.
- */
- SetLuminances() and SetLuminancesAndRange() both produce a linear relationship
- between CLUT entry and luminance over the range firstLuminance to lastLuminance,
- with minimum error relative to any luminance in the range lowLuminance to highLuminance.
- (We don't care about a small error in mean luminance.)
- (SetLuminances, which doesn't have these arguments, uses firstLuminance and lastLuminance
- to set this range.) The three DACs are linear, and the video attenuator will combine
- them linearly yielding a voltage V at the display input. This allows you to
- compute your image (e.g. a sinewave grating) and loaded to the frame store
- as a full scale linear function, i.e. with values ranging 0..255. Linearization
- of the display and setting of contrast to whatever you want are both
- accomplished by one call to SetLuminances(). The contrast can be changed by
- calling SetLuminances() again, now with a larger or smaller luminance range. If
- SetLuminances is too slow for a real-time application (its computations take
- about 8-30 ms to compute a whole 256-entry CLUT) you can tell it to just compute,
- but not load the new ColorSpec table (just supply NULL in place of myGDHandle), and
- you can then quickly load your ColorSpec table into the CLUT later by calling
- LoadLuminances().
-
- double GetLuminance(GDHandle myGDHandle,luminanceRecord *LP,int theEntry)
- /*
- If myGDHandle is not NULL then examines one entry in the actual CLUT, otherwise examines
- the ColorSpec table contained in *LP, and in either case
- returns the luminance that will be produced. Supplying an illegal entry value results in
- a returned value of -INF.
- */
- GetLuminance() returns the luminance of a single entry. If myGDHandle is not NULL then
- GetLuminance will make a low-level call to the video driver to determine what is in that
- CLUT entry in the actual hardware, and will return the luminance that is expected
- to result given the channel gains and luminance nonlinearity described in your
- luminanceRecord. If myGDHandle is NULL then GetLuminance
- returns the luminance corresponding to the entry in the ColorSpec table in your
- luminanceRecord.
-
- void IncrementLuminance(GDHandle myGDHandle,luminanceRecord *LPtr,int theEntry)
- /*
- Make smallest possible increase of the luminance of one entry in the ColorSpec table.
- This is a way to figure out what is the lowest contrast that you can produce.
- */
-
- void LoadLuminances(GDHandle myGDHandle, luminanceRecord *LP,
- int firstEntry, int lastEntry)
- /*
- This just calls GDSetEntries() to load your ColorSpec table into the CLUT of
- your screen device. It is here simply to provide a cosmetic match to the
- call to SetLuminances(), for which loading the CLUT is optional.
- Note: if you prefer, instead of LP you may send just the address of
- a ColorSpec table, cast to (luminanceRecord *), since a luminanceRecord
- begins with a ColorSpec table.
- */
-
- double GetV(GDHandle myGDHandle,luminanceRecord *LP,int theEntry);
- double VToL(luminanceRecord *LP,double V);
- double LToV(luminanceRecord *LP,double L);
- double LToL(luminanceRecord *LP,double L);
- Most users will never have any reason to use these routines. They are useful
- primarily in error analysis of the Luminance.c package.
-
- EXAMPLES:
-
- /* load CLUT with linear luminance range, immediately */
- tolerance=SetLuminances(myGDHandle,&LR,0,255,(1.-c)*meanLuminance,(1+c)*meanLuminance);
-
- /* load ColorSpec table with linear luminance range, then load CLUT */
- tolerance=SetLuminances(NULL,&LR,0,255,(1.-c)*meanLuminance,(1+c)*meanLuminance);
- LoadLuminance(myGDHandle,&LR,firstEntry,lastEntry);
-
- /* load ColorSpec table with sinusoidal luminance range, then load CLUT */
- for(entry=0;entry<256;entry++) {
- luminance = meanLuminance*(1.0+c*sin(2.0*3.14159265*entry/256.));
- tolerance=SetLuminance(NULL,&LR,entry,luminance,(1.-c)*meanLuminance,(1+c)*meanLuminance);
- }
- LoadLuminance(myGDHandle,&LR,firstEntry,lastEntry);
-
- /* Examine an entry in the ColorSpec table or CLUT */
- luminance=GetLuminance(myGDHandle,&LR,entry); /* in CLUT */
- luminance=GetLuminance(NULL,&LR,entry); /* in ColorSpec table */
-
- NOTES:
-
- The argument firstEntry must not exceed lastEntry. They may be equal.
-
- The returned value, tolerance, is an estimate of the largest possible error in the
- luminance DIFFERENCES, taking into account the precision,
- accuracy (assumed to be ±1 least significant bit), and range of the DACs.
- (Note the returned tolerance tells you about errors in the luminance DIFFERENCES on
- the display. The error in ABSOLUTE luminance of the display is expected to be at
- most ±1 step of all the DACs, i.e. one part in 255 of full scale.)
-
- There are no restrictions at all on firstLuminance and lastLuminance.
- It is reasonable to request an impossibly large luminance range, e.g. from a
- negative luminance up to twice the maximum luminance. SetLuminances will produce the
- closest possible approximation. The luminance of each entry will be clipped
- to fit in the range of possible luminances. Thus you will obtain the requested
- contrast for pixel values that aren't clipped. You can detect the clipping by the fact
- that the returned tolerance will be very large.
-
- A common mistake in C programming is to use a pointer that supposedly points to a
- structure, without ever allocating said structure. Consider the luminanceRecord.
- The following declaration and call will be accepted by the compiler, but will
- usually have disastrous consequences:
- luminanceRecord *LP;
- SetLuminances(...,LP,...); /* Bad! */
- The problem is that you never allocated a luminanceRecord, only a pointer. When
- SetLuminances starts storing information in what it thinks is a luminanceRecord it
- will actually be writing over a random part of memory. What you should do is:
- luminanceRecord LR;
- SetLuminances(...,&LR,...); /* Good */
- Or you can do this:
- luminanceRecord LR,*LP;
- LP=&LR;
- SetLuminances(...,LP,...); /* Good */
-
- Before using SetLuminance or SetLuminances you must initialize your luminanceRecord.
- I suggest you do that by #including a file with all the parameters describing
- your monitor.
- luminanceRecord LR;
- #include "LuminanceRecord2.h"
-
- Here's an example of the contents of a LuminanceRecord2.h file, as produced by the
- program CalibrateLuminance. You must use CalibrateLuminance to calibrate
- your own framestore, ISR Video Attenuator, and display.
- LR.screen=2; /* myGDHandle=GetScreenDevice(LR.screen); */
- LR.date="10:20 AM Wednesday, October 10, 1990";
- LR.id="5111769";
- LR.name="noise";
- LR.notes="denis, lights off";
- LR.dpi=76.0; /* pixels per inch */
- LR.Hz=66.67; /* frames per second */
- LR.units="cd/m^2";
- /* coefficients of polynomial fit */
- LR.coefficients=9; /* # of coefficients in polynomial fit */
- /* L(V)=p[0]+p[1]*V+p[2]*V*V+ . . . ±polynomialError */
- LR.p[0]=-2.28448e-15;
- LR.p[1]=-1.47149e-13;
- LR.p[2]=-8.91074e-12;
- LR.p[3]=-4.47497e-10;
- LR.p[4]=-1.42193e-08;
- LR.p[5]=4.5072e-10;
- LR.p[6]=-1.41595e-12;
- LR.p[7]=-1.16427e-15;
- LR.p[8]=6.69633e-18;
- LR.polynomialError= 0.1974; /* RMS error of fit */
- /* coefficients of quadratic fit */
- /* L(V)=q[0]+q[1]*V+q[2]*V*V±quadraticError */
- LR.q[0]=5.72078;
- LR.q[1]=-0.261753;
- LR.q[2]=0.00204155;
- LR.quadraticError= 2.1642; /* RMS error of fit */
- /* coefficients of power law fit */
- /* L(V)=power[0]+Rectify(power[1]+power[2]*V)^power[3]±powerError */
- /* Rectify(x)=x if x≥0, Rectify(x)=0 if x<0 */
- /* Pelli & Zhang (1991) Eqs.9&10 use symbols v=V/255, alpha=power[0], beta=power[1], kappa=power[2]*255, gamma=power[3] */
- LR.power[0]=0.396008;
- LR.power[1]=-2.50082;
- LR.power[2]= 0.035;
- LR.power[3]=2.31643;
- LR.powerError= 0.0863; /* RMS error of fit */
- /* coefficients of power law fit, with fixed exponent */
- /* L(V)=fixedPower[0]+Rectify(fixedPower[1]+fixedPower[2]*V)^fixedPower[3]±fixedPowerError */
- LR.fixedPower[0]=0.422447;
- LR.fixedPower[1]=-2.68117;
- LR.fixedPower[2]=0.0364552;
- LR.fixedPower[3]= 2.28;
- LR.fixedPowerError= 0.0796; /* RMS error of fit */
- LR.r=0.0301291;
- LR.g=0.14587;
- LR.b=0.824001;
- LR.gainAccuracy=-0.00241191;
- LR.gm=3.39315; /* The monitor's contrast gain. */
- LR.VMin= 0; /* minimum nominal voltage that can be loaded into DAC */
- LR.VMax=255; /* maximum nominal voltage that can be loaded into DAC */
- LR.LMin= 0.40; /* luminance at VMin */
- LR.LMax= 74.74; /* luminance at VMax */
- LR.LBackground= 12.404; /* background luminance during calibration */
- LR.VBackground=155; /* background number used during calibration */
- LR.rangeSet=0; /* indicate that range parameters have yet to be set */
- LR.L.exists=0; /* indicate that luminance table has yet to be initialized */
- Note that these parameters describe fits of three functions to the gamma function.
- You may omit any or all of these fits, but should document it by setting the
- corresponding error terms to infinity (=1.0/0.0). If you omit all of them then
- you must supply a table describing the gamma function, e.g.
- LR.L._VMin=DoubleToMilli(0.); /* if possible, restrict to just the monotonic range */
- LR.L._VMax=DoubleToMilli(255.); /* if possible, restrict to just the monotonic range */
- LR.L._dV=(LR.L._VMax-LR.L._VMin)/(LUMINANCES_IN_TABLE-1);
- LR.L.latestIndex=-1; /* invalid latestIndex, so hunt will start from scratch */
- LR.L.exists=luminanceSet; /* mark table as valid */
- LR.L._L[0]=DoubleToMilli(0.57);
- LR.L._L[1]=DoubleToMilli(0.57);
- ...
- LR.L._L[255]=DoubleToMilli(101.13);
- Normally this table is synthesized at run time, from one of the formulaic descriptions,
- but you may prefer to supply it directly, possibly using the raw measurements,
- and not bother with any fitting. The formulas are not used if the table is supplied.
-
- To fill in the numbers above, you have to do two calibrations. (This is what the
- CalibrateLuminance program does.)
-
- 0. Before starting, call GDLinearGamma(). (Note that GDOpenWindow
- automatically calls GDLinearGamma for you.) All calibrations should be done
- with the ISR Video Attenuator in place.
-
- 1. Measure the luminance versus "voltage" nonlinearity of your monitor.
- I use a separate program "CalibrateLuminance" to measure the screen luminance at values
- of V from 0 to 255. For this calibration you load equal values into Red, Green, and Blue
- lookup tables. I use quadratic, polynomial, and power law fits. See Pelli & Zhang (1991).
-
- You must set LR.LMin and LR.LMax to the luminances at LR.VMin and LR.VMax.
-
- The following program fragment allows you to measure the screen luminance as a function
- of nominal voltage.
-
- int V,i,done;
- double a;
-
- #include "LuminanceRecord1.h"
- oldGDHandle=GetGDevice();
- GetPort(&oldWindowPtr);
- LR.screen=1;
- myGDHandle=GetScreenDevice(LR.screen); /* screen 1 */
- myCWindowPtr=GDOpenWindow(myGDHandle); /* Open a full-screen window with explicit colors */
- SetPort((WindowPtr)myCWindowPtr); /* Tell QuickDraw which window */
- SetGDevice(myGDHandle); /* specify which device's color table */
- PmBackColor(1); /* pick a color table entry */
- EraseRect(&myCWindowPtr->portRect); /* fill whole window with that color */
- SetPort(oldWindowPtr);
- SetGDevice(oldGDHandle);
- LR.L._VMin=DoubleToMilli(0.); /* if possible, restrict to just the strictly monotonic range */
- LR.L._VMax=DoubleToMilli(255.);
- done=0;
- for(i=0;;i++){
- LR.L._dV=ceil(MilliToDouble(LR.L._VMax-LR.L._VMin)/(LUMINANCES_IN_TABLE-1));
- V=MilliToLong(i*LR.L._dV);
- if(V>=MilliToLong(LR.L._VMax)){
- V=MilliToLong(LR.L._VMax);
- done=1;
- }
- LR.table[1].rgb.red =V<<8; /* Set color table entry 1. */
- LR.table[1].rgb.green =V<<8;
- LR.table[1].rgb.blue =V<<8;
- LoadLuminances(myGDHandle,&LR,1,1); /* Copy color table entry 1 to CLUT */
- printf("%3d Please measure screen luminance now, in %s, and type in:",V,LR.units);
- gets(string);
- sscanf(string,"%lf",&a);
- printf("%6.2g %s\n",a,LR.units);
- LR.L._L[i]=DoubleToMilli(a);
- if(done)break;
- if(LR.L._L[i]<=LR.L._L[0]){ /* restrict to just the strictly monotonic range */
- LR.L._VMin=LongToMilli(V);
- LR.L._L[0]=LR.L._L[i];
- i=0;
- }
- }
- LR.L.latestIndex=-1; /* invalid latestIndex, so hunt will start from scratch */
- LR.L.exists=luminanceSet; /* mark table as valid */
-
- 2. Measure the gains of the three inputs of your ISR Video Attenuator. You could
- do this by using an oscilloscope to measuring the voltage at the input to the monitor.
- Instead, I measure the resulting luminance and use LToV() to infer the voltage.
- Vary one DAC while holding the other two fixed at 255 and measure the luminances.
- Use the function LToV() to convert back to a "voltage" and compute the gain of the
- varying DAC. Note that the three DACs on your framestore will generally have gains that
- match to only ±5%. This calibration is measuring the overall gain of each of the three
- pathways, including your DACs and the ISR Video Attenuator.
-
- LIMITATIONS:
-
- It is imperative that you call GDLinearGamma() at some time before using
- SetLuminances. SetLuminances ASSUMES that no gamma correction takes place. You only
- need to call GDLinearGamma once. It will stay that way until the next time you
- restart your computer. Incidentally, GDOpenWindow() calls GDLinearGamma for you.
-
- The luminance calibration data that you supply to SetLuminances must also have been
- collected with no gamma correction, i.e. AFTER calling GDLinearGamma.
-
- The reason that gamma correction is not allowed is that it would result in a nonlinear
- transformation of the three channels BEFORE they are combined in the Video Attenuator.
-
- The measurement of the gains of the three pathways must be made using YOUR framestore.
- The gains of your three DACs will in general be different from each
- other, and different from framestore to framestore. Brooktree
- guarantees the matching of the gains of the three DACs on their chip to only ±5%.
-
- The luminance record may include either a table of luminance calibrations. If it is
- not supplied then it will be synthesized from the parameters of the polynomial or power
- law fit. If the power, polynomial, or quadratic fit parameters are
- not supplied then the appropriate LR.powerError, LR.polynomialError, or LR.quadraticError
- field should be set to infinity (=0.0/1.0).
-
- The whole package is at present restricted to grayscale.
- There is no provision for linearizing a color monitor. In particular it is assumed that
- the the DACs are linearly combined BEFORE the display nonlinearity. Linearizing luminance
- on a color display would need to allow for three different nonlinear transformations.
-
- EXPLANATION OF THE CODE:
-
- Physically your framestore transforms each CLUT entry number to a voltage that will be
- linearly related to the number. The three output voltages will be combined by the ISR
- Video Attenuator to produce a single voltage which drives the video monitor. Finally,
- the display nonlinearly transforms V to produce a luminance L. My convention for
- "measuring" the "voltage" V at the input of the monitor is that V=0 when all three
- DACs are set to zero and V=255 when all three DACs are set to 255. (This will be linearly
- related to volts measured, with a voltmeter, at the output of the Video Attenuator.)
- The virtue of the attenuator is that it allows us to produce nonintegral values of V.
-
- There are two givens:
- 1. DACs are inaccurate. A good DAC may be specified to be merely monotonic. For purposes
- of computing the returned tolerance value, I follow the Brooktree DAC specification of
- ±1 LSB error in integrated linearity and assume
- that the error in any luminance difference is at most ±1 LSB.
- 2. We want to minimize the error in representing the waveform, but are not particularly
- worried about the exact value of the mean luminance. Thus, if we have DACs with different
- gains we may fix the coarse DACs to set the mean luminance and vary the fine DACs to
- produce the linear range of luminances requested.
-
- Here's the strategy. We want to cover the range L0 to Ln with the smallest possible
- error in L-L0.
-
- Steps 1 to 5 happen in SetLuminanceRange:
- 1. Sort the DACs by gain g, where gain is defined as the change in V when
- a single DAC is increased from 0 to 255, so g0+g1+g2=1. Let the gains be g0>g1>g2.
- 2. Transform the luminance range to a nominal voltage range lowV and highV.
- 3. Decide which DACs should be variable and which should be fixed,
- so as the minimize the tolerance in the luminance increments.
- 4. Temporarily set the variable DACs to their midpoints. Now set the fixed DACs to
- most accurately represent the midpoint of the nominal voltage range, (lowV+highV)/2.
- 5. If necessary, compute a small luminance offset LShift to bring the requested range
- lowV to highV into the range attainable by the variable DACs. (This is necessary
- because the centering in step 4 may not be precise enough.)
-
- Step 6 happens in _SetLuminance:
- 6. Set the variable DACs in the ColorSpec entry so as to most accurately represent
- L+LShift.
-
- SetLuminances(), SetLuminancesAndRange(), and SetLuminance() all call _SetLuminance().
-